/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2021-03-15
 * V4.0
 */
package com.jphenix.servlet.filter;

//#region 【引用去】
import java.util.List;
import java.util.Map;

import com.jphenix.driver.nodehandler.util.ParentChildNodeVO;
import com.jphenix.driver.regc.RegistCenterFilter;
import com.jphenix.driver.threadpool.ThreadSession;
import com.jphenix.service.nodeloader.NodeService;
import com.jphenix.servlet.common.ActionContext;
import com.jphenix.servlet.parent.ServiceBeanParent;
import com.jphenix.share.printstream.PrintWriterTool;
import com.jphenix.share.util.BaseUtil;
import com.jphenix.share.util.StringUtil;
import com.jphenix.standard.docs.BeanInfo;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.docs.Running;
import com.jphenix.standard.servlet.IActionBean;
import com.jphenix.standard.servlet.IActionContext;
import com.jphenix.standard.servlet.IFilter;
import com.jphenix.standard.servlet.IRequestManager;
import com.jphenix.standard.servlet.IResponseManager;
import com.jphenix.standard.servlet.api.IFilterConfig;
import com.jphenix.standard.servlet.api.IRequest;
import com.jphenix.standard.servlet.api.IResponse;
import com.jphenix.standard.viewhandler.INodeHandler;
import com.jphenix.standard.viewhandler.IViewHandler;
//#endregion

//#region 【类说明】
/**
 * 页面（模板）请求过滤器
 * com.jphenix.servlet.filter.PageFilter
 * 
 * 默认从 /WEB-INF/page_model 路径下加载
 * 
 * 注意：取消了通过uri  xxx.htm 加载模板，而是使用动态扩展名（默认为.ha）
 * 
 * 2021-04-27 解决了无法解析.ha扩展名的模板文件错误
 * 2021-09-22 屏蔽了输出警告日志时，打出堆栈信息
 * 2022-09-04 隔离了ServletApi，兼容新老Tomcat
 * 2022-09-05 默认从/WEB-INF/page_model路径加载模板文件。在url请求中取消htm扩展名加载模板
 * 2024-06-06 取消了page_model_from_web_base_path参数，如果model_base_path存在值，则从这个路径下获取模板，为空从网站根路径下获取模板
 * 2024-06-06 修改了代码错误
 * 
 */
//#endregion
@Running({"90"})
@BeanInfo({"pagefilter"})
@ClassInfo({"2024-07-14 12:31","页面（模板）请求过滤器"})
public class PageFilter extends ServiceBeanParent implements IFilter {

	//#region 【声明区】
	private String             encoding        = null;  // 页面编码
	private NodeService        ns              = null;  // 页面对象服务
	private RegistCenterFilter rcf             = null;  // 注册中心过滤器
	private String[]           allowOriginVals = null;  // 允许跨站的域名数组，*为全部域名
	private String             modelBasePath   = null;  // 模板根路径（相对网站根路径）
	private String             modelExtName    = "htm"; // 模板扩展名
	//#endregion

	//#region getIndex() 获取排序索引，数值越小越先执行
	/**
	 * 覆盖方法
	 * 刘虻
	 * 2021年03月15日
	 */
	@Override
	public int getIndex() {
		return 190;  //优先级最低，通常都是最后执行
	}
	//#endregion

	//#region getFilterActionExtName() 获取需要过滤的动作路径扩展名
	/**
	 * 覆盖方法
	 * 刘虻
	 * 2021年03月15日
	 */
	@Override
	public String getFilterActionExtName() {
		return "ha";
	}
	//#endregion

	//#region init(bf,config) 执行初始化
	/**
	 * 执行初始化
	 * @param fe         过滤器管理类
	 * @param config     Servlet配置信息类
	 * @throws Exception 异常（如果初始化发生异常，则放弃不再使用）
	 * 2021年03月15日
	 * @author MBG
	 */
	@Override
	public void init(BaseFilter bf, IFilterConfig config) throws Exception {
		this.encoding = config.getInitParameter("encoding");
		if(this.encoding==null || this.encoding.length()<1) {
			this.encoding = "UTF-8";
		}
		modelBasePath = p("model_base_path");
		ns  = bean(NodeService.class);
		rcf = bean(RegistCenterFilter.class);
	}
	//#endregion

	//#region doFilter(req,resp) 执行过滤
	/**
	 * 覆盖方法
	 * 刘虻
	 * 2021-03-15 下午18:14:09
	 */
	@Override
	public boolean doFilter(IRequestManager req, IResponseManager resp) throws Exception {
		req.setCharacterEncoding(encoding);
		try {
			//强制输出编码格式为UTF-8
			resp.setCharacterEncoding("UTF-8");
		}catch(Error e) {}
		catch(Exception e) {}

		//动作路径
		String action = req.getServletPath();
		//去掉扩展名
		int point = action.lastIndexOf(".");
		if(point>-1) {
			action = action.substring(0,point);
		}
		//动作上下文
		IActionContext ac = newActionContext(action,req,resp);

		//放入动作路径和回话主键
		ThreadSession.put("_action_",action);                            //动作路径
		ThreadSession.put("_session_key_",ac.getSessionID()); //会话主键

		//清除缓存
		ac.getResponse().setHeader("Pragma","No-cache");
		ac.getResponse().setHeader("Cache-Control","no-cache");
		ac.getResponse().setDateHeader("Expires", 0);

		//解决跨域访问session丢失问题
		ac.getResponse().setHeader("P3P","CP=CAO PSA OUR"); 

		String[] vals = getAllowOriginVals(); //获取允许跨站域名数组
		for(int i=0;i<vals.length;i++) {
			ac.getResponse().setHeader("Access-Control-Allow-Origin",vals[i]); 
		}
		//设置页面类型，有些浏览器，如果不设置页面内容类型，则直接将代码输出到页面上
		ac.getResponse().setContentType("text/html");

		//构建视图文件路径
		String viewPath = modelBasePath+action+"."+modelExtName;
		//为反向动作调用
		//构建返回主界面
		INodeHandler nh = ns.getNodeHandlerByPath(ac,viewPath);
		if(nh==null || nh.isEmpty()){
			log.warning("Not Find The Model Of Action:["
					+action+"] BasePath:["+modelBasePath+"} Path:["+viewPath
					+"] Referer:["+ac.getRequest().getHeader("Referer")+"]",null);
			ac.getResponse().sendError(IResponse.SC_NOT_FOUND,ac.getRequest()); 
			return true;
		}
		//url中的请求参数字符串
		String queryStr = req.getQueryString();
		//不输出日志 （某些动作不输出日志，比如日志事件触发标记日志输出准备完毕的动作）
		if(queryStr.indexOf("_nolog_")>0) {
			outLog(false);
		}
		//之前遇到个无法呈现的问题，偶尔输出内容全是问号
		//原来使用的是vh.getDealEncode();这回写死UTF-8
		//基本输出编码 weblogic只能用基本编码输出
		String standardOutEncoding = "UTF-8";
		try {
			//设置编码格式
			ac.getResponse().setCharacterEncoding(standardOutEncoding);
		}catch(NoSuchMethodError e) {
			//使用WebLogic时会抛异常到这里，然后使用标准输出
			//非weblogic不能使用标准输出，否则反而出现乱码
			standardOutEncoding = "ISO-8859-1";
		}
		//获取动作对应的方法名
		Object time = log.runBefore(); //标记执行开始时间
		info("Start Execute PageAction:["+action+"]");
		try{
			parseNode(ac,nh); //执行解析模板
			//输出信息到页面
			nh.getNodeBody(
					new PrintWriterTool(
							ac.getResponse().getWriter(),
							nh.getDealEncode(),
							standardOutEncoding,
							false));
			//不能输出html代码到页面，太乱
		}catch(Exception e) {
			e.printStackTrace();
		}finally{
			log.writeRuntime(time,"Execute Complete Action:["+action+"]");
			outLog(true); //如果采用了线程池，需要手动重置这个值，否则就会出现有时无法输出日志的问题
		}
		return true;
	}
	//#endregion

	//#region 【内部方法】getAllowOriginVals() 处理 Access-Control-Allow-Origin
	/**
	 * 处理 Access-Control-Allow-Origin
	 * @return 允许跨站域名
	 * 2019年6月28日
	 * @author MBG
	 */
	private String[] getAllowOriginVals() {
		if(allowOriginVals==null) {
			//获取允许跨站域名信息
			String val = p("servlet_access_control_allow_origin");
			if(val.length()<1) {
				allowOriginVals = new String[0];
			}else {
				allowOriginVals = BaseUtil.split(val,",",";","，","；");
			}
		}
		return allowOriginVals;
	}
	//#endregion

	//#region 【内部方法】newActionContext(action,req,resp) 构造新的动作上下文
	/**
	 * 构造新的动作上下文
	 * @param action 动作路径
	 * @param req    请求对象
	 * @param resp   返回对象
	 * @return       动作上下文
	 */
	private ActionContext newActionContext(String action,IRequest req,IResponse resp){
		return new ActionContext(req,resp,req.getServerName().toLowerCase(),action,uploadPath());
	}
	//#endregion

	//#region parseNode(action,req,resp,nh) 反向解析动作页面
	/**
	 * 反向解析动作页面
	 * 刘虻
	 * 2010-6-8 下午05:02:15
	 * @param actionContext 动作上下文
	 * @param nh 视图对象
	 * @throws Exception 执行发生异常
	 */
	public void parseNode(String action,IRequest req,IResponse resp,IViewHandler nh) throws Exception {
		parseNode(newActionContext(action,req,resp),nh);
	}
	//#endregion

	//#region parseNode(actionContext,nh) 反向解析动作页面
	/**
	 * 反向解析动作页面
	 * 刘虻
	 * 2010-6-8 下午05:02:15
	 * @param actionContext 动作上下文
	 * @param nh 视图对象
	 * @throws Exception 执行发生异常
	 */
	public void parseNode(IActionContext actionContext,IViewHandler nh) throws Exception {
		Object time = log.runBefore(); //标记执行开始时间
		//获取执行页面的主子信息类
		ParentChildNodeVO pcn = ParentChildNodeVO.fixParentChildNodeVOByAttribute(nh,"_action");
		parseNode(actionContext,pcn); //准备递归调用解析模板
		log.writeRuntime(time,"Parse Html Use Time");
	}
	//#endregion

	//#region 【内部方法】parseNode(actionContext,pcn) 反向解析动作页面 (递归函数)
	/**
	 * 反向解析动作页面 (递归函数)
	 * 刘虻
	 * 2010-6-8 下午05:02:15
	 * @param actionContext 动作上下文
	 * @param nh 视图对象
	 * @return  解析完当前段后，还要不要往下解析
	 * 通常都会继续往下解析，除非遇到了执行某个块动作时，
	 * 重新加载了整个页面，也就没有必要继续解析以前旧的内容了
	 * @throws Exception 执行发生异常
	 */
	private boolean parseNode(
			IActionContext actionContext,ParentChildNodeVO pcn) throws Exception {
		if(pcn==null) {
			return true;
		}
		if(pcn.hasChild()) {
			List<ParentChildNodeVO> cNodeList = pcn.getChildList(); //获取子节点
			for (int i=0;i<cNodeList.size();i++) {
				if(!parseNode(actionContext, cNodeList.get(i))) {
					return false;
				}
			}
		}
		//获取子标签元素
		IViewHandler cNh = pcn.getView();
		//移出标签
		cNh.removeAttribute("_execute");
		//是否隐藏声明标签
		if(boo(cNh.getAttribute("_hiddenself")) 
				|| boo(cNh.getAttribute("_hidden")) ) {
			cNh.setOutSelf(false);
		}
		return fixPageAction(actionContext,pcn);
	}
	//#endregion

	//#region 【内部方法】 fixPageAction(ac,pcn) 处理动作节点
	/**
	 * 处理动作节点
	 * 刘虻
	 * 2010-6-9 下午06:42:49
	 * @param ac  动作上下文
	 * @param pcn 节点模块
	 * @return 执行完当前动作后，能否继续执行下一个动作
	 * 通常都会继续往下解析，除非遇到了执行某个块动作时，
	 * 重新加载了整个页面，也就没有必要继续解析以前旧的内容了
	 * @throws Exception 执行发生异常
	 */
	private boolean fixPageAction(
			IActionContext     ac
			,ParentChildNodeVO pcn) throws Exception {
		IViewHandler cVh = pcn.getView(); //获取待处理的子节点
		//获取动作路径
		String action = cVh.ra("_action");
		if(action.length()<1) {
			return true;
		}
		log("Parse Model Page Execute Action:["+action+"]");
		int point = action.indexOf("?"); //参数分隔符
		if(point>0) {
			//模板中调用该动作传入的固定参数，该参数优先顺序排在url传入参数之后
			Map<String,String> modelParaMap = StringUtil.fixQueryString(action.substring(point+1));
			action = action.substring(0,point);
			List<String> keyList = BaseUtil.getMapKeyList(modelParaMap); //参数主键序列
			for(String key:keyList) {
				if(!ac.hasParameter(key)) {
					ac.setParameter(key,new String[] {str(modelParaMap.get(key))});
				}
			}
		}
		//脚本主键
		String scriptId = action.toUpperCase();
		if(!action.startsWith("/")) {
			action = "/"+action;
		}
		point = scriptId.lastIndexOf("/"); //分隔符
		if(point>-1) {
			scriptId = scriptId.substring(point+1);
		}
		//检查相关节点
		String relateIds = cVh.a("_relateids");
		if(relateIds.length()>0) {
			//分割成序列
			List<String> relateIdList = BaseUtil.splitToList(relateIds,",");
			IViewHandler tVh          = ns.getNewViewHandler(); //构建抽象类
			tVh.setOutSelf(false);
			tVh.addChildNode(cVh);
			//获取当前节点的根节点
			IViewHandler baseVh = cVh.getBaseNode();
			for(String relateId:relateIdList) {
				if(relateId==null || relateId.length()<1) {
					continue;
				}
				//获取对应的相关节点
				IViewHandler vh = baseVh.getFirstChildNodeByID(relateId);
				if(vh.isEmpty()) {
					continue;
				}
				tVh.addChildNode(vh);
			}
			cVh = tVh;
		}
		//动作类
		IActionBean cab = rcf.getActionScript(action,scriptId,ac,null);
		if(cab==null) {
			return false;
		}
		//设置传入参数
		cab.setModelParameterMap(
				StringUtil
				.getParaMapFromString(
						cVh.getAttribute("_para")));
		cVh.removeAttribute("_para");
		//获取是否不使用内置html
		boolean noInnerHtml = boo(cVh.getAttribute("_nohtml"));
		cVh.removeAttribute("_nohtml");
		if (!noInnerHtml) {
			//设置操作主界面
			cab.setReVh(cVh);
		}
		//执行脚本动作
		rcf.executeAction(action,cab,ac,null);
		return !cab.isOver();
	}
	//#endregion
	
	//#region destroy() 终止该过滤器
	/**
	 * 覆盖方法
	 */
	@Override
	public void destroy() {}
	//#endregion
}








